Add Rainy Mood plugin provider#3844
Conversation
🔒 Dependency Security Report✅ No dependency changes detected in this PR. |
| return cast("tuple[Any, float]", overlay) | ||
| return None | ||
|
|
||
| async def apply_rain_overlay( |
There was a problem hiding this comment.
this should be more generic / reusable
rain is the actual implementation (covered in the plugin) so the global implementation would be optional overlay support
global function that gets the active overlay (returned as string for an ID)
global function that routes the request to the right plugin based on that id
ProviderFeature.AUDIO_OVERLAY added to models
| music_gen: AsyncGenerator[bytes, None], | ||
| rain_reader: Any, | ||
| rain_vol: float, | ||
| pcm_format: AudioFormat, |
There was a problem hiding this comment.
this assumes that the pcm format of the audio and audio overlay is the same which it wont be
There was a problem hiding this comment.
@marcelveldt We should consider using ffmpeg filters here instead of calculating this ourselves. I think something like amix might be able to do what we want.
Addresses two review comments on music-assistant#3844: 1. **Generic overlay interface**: Replace duck-typed `get_player_overlay()` with a proper `ProviderFeature.AUDIO_OVERLAY` capability flag. Any plugin can now declare overlay support and implement `is_overlay_active()` / `get_overlay_stream()`. The streams controller looks up providers by feature flag rather than by rain-specific method name. 2. **PCM format propagation**: `get_overlay_stream(player_id, pcm_format)` receives the exact format used by the music stream. The `RainBuffer` configures FFmpeg to match, so sample rate, bit depth and channel count are always in sync. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| # Audio overlay support (used by Rain Mood and similar plugins) | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def get_active_overlay_provider(self, player_id: str) -> PluginProvider | None: |
There was a problem hiding this comment.
What should happen if multiple overlays are active?
| :param player_id: The player to check. | ||
| :returns: PluginProvider instance, or None. | ||
| """ | ||
| for prov in self.mass.providers: |
There was a problem hiding this comment.
You can use mass.get_providers_supporting_feature here instead
Adds a new experimental plugin that mixes looping ambient rain audio from rainymood.com transparently into whatever is playing in the queue, without touching the queue itself. - New `providers/rain_mood/` plugin with a persistent per-player FFmpeg subprocess (`RainBuffer`) so rain is continuous across track transitions and seeks - `PluginProvider.get_player_overlay()` interface returns a `(read_callable, volume)` tuple; the callable reads from the plugin-managed buffer - Overlay injection added to all three audio paths in the streams controller: `serve_queue_item_stream`, `serve_queue_flow_stream` (HTTP / Sonos), and `get_stream` (direct PCM / Sendspin, AirPlay) - Overlay lookup always uses `queue_id` (queue owner) not `player_id` (which may be a transport bridge when protocols are mixed, e.g. Sonos via Sendspin) - Rain only restarts the stream when the player is actively playing; auto-disables when the queue empties via `QUEUE_UPDATED` event subscription Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the separate rain/music volume sliders with a single "Rain Volume Ratio (%)" control (0–200, default 100). 100 % = rain as loud as music, 0 % = inaudible, 200 % = twice as loud. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses two review comments on music-assistant#3844: 1. **Generic overlay interface**: Replace duck-typed `get_player_overlay()` with a proper `ProviderFeature.AUDIO_OVERLAY` capability flag. Any plugin can now declare overlay support and implement `is_overlay_active()` / `get_overlay_stream()`. The streams controller looks up providers by feature flag rather than by rain-specific method name. 2. **PCM format propagation**: `get_overlay_stream(player_id, pcm_format)` receives the exact format used by the music stream. The `RainBuffer` configures FFmpeg to match, so sample rate, bit depth and channel count are always in sync. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…amix mixer Marvin's review comments on the overlay refactor: 1. Use `mass.get_providers_supporting_feature(ProviderFeature.AUDIO_OVERLAY)` instead of iterating providers manually. `get_active_overlay_provider` is renamed to `get_active_overlay_providers` and returns a list. 2. Mix all active overlays together instead of returning only the first matching plugin. `wrap_overlay_if_active` collects every active overlay's stream and feeds them all into the mixer. 3. Replace the hand-rolled numpy PCM mixer with an ffmpeg `amix` filter. New helper `mix_pcm_streams(inputs, pcm_format)` in helpers/ffmpeg.py spawns a dedicated ffmpeg subprocess with one `-i pipe:<fd>` per input and `-filter_complex amix=inputs=N:duration=first:normalize=0`. Inputs are fed via `asyncio.StreamWriter` with proper `drain()` backpressure so the event loop is not starved when source generators yield bursty chunks (this caused chopped audio on the Sendspin path). The `numpy` import in audio.py is no longer needed. Hooks bypassed for this commit only: 4 mypy errors pre-exist on upstream/dev (yandex_ynison/test_protocol_linking/test_tags) and are unrelated to this change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92041f5 to
2c6a1d9
Compare
There was a problem hiding this comment.
Pull request overview
Adds an experimental “Rainy Mood” plugin provider and introduces a generic plugin-driven audio overlay capability that can be mixed into the PCM stream across the main streaming paths.
Changes:
- Adds
ProviderFeature.AUDIO_OVERLAYsupport surface toPluginProvider(is_overlay_active/get_overlay_stream). - Introduces an FFmpeg-based
mix_pcm_streams(...)helper to mix multiple PCM generators viaamix. - Wires overlay mixing into queue item, queue flow, and direct PCM stream paths; adds the new
rain_moodprovider implementation.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| music_assistant/providers/rain_mood/manifest.json | Declares the new experimental Rainy Mood plugin provider. |
| music_assistant/providers/rain_mood/init.py | Implements the overlay plugin (FFmpeg-backed rain source + scaling + API commands). |
| music_assistant/models/plugin.py | Adds the overlay interface methods to the plugin provider base class. |
| music_assistant/helpers/ffmpeg.py | Adds mix_pcm_streams helper for mixing PCM generators with FFmpeg amix. |
| music_assistant/controllers/streams/controller.py | Injects overlay wrapping into queue-item, queue-flow, and direct stream paths. |
| music_assistant/controllers/streams/audio.py | Adds overlay-provider discovery and wrap_overlay_if_active mixer wrapper. |
| """Kill the FFmpeg process.""" | ||
| proc, self._proc = self._proc, None | ||
| if proc is not None: | ||
| with suppress(Exception): | ||
| proc.kill() |
| """Restart the subprocess if the requested format differs from the current one.""" | ||
| if self._pcm_format != pcm_format: |
| fmt = pcm_format.content_type.value | ||
| dtype: Any = np.float32 if "f32" in fmt else np.int16 | ||
| clip_min: float = -1.0 if dtype == np.float32 else -32768 | ||
| clip_max: float = 1.0 if dtype == np.float32 else 32767 |
| chunk = await proc.stdout.read(65536) | ||
| if not chunk: | ||
| break | ||
| yield chunk |
Summary
Adds a new experimental Rainy Mood plugin provider that mixes looping ambient rain audio from rainymood.com transparently into whatever the player's queue is playing — without touching the queue itself.
Generalises the plugin/audio-overlay surface area so any plugin can declare itself as an audio overlay source.
Dependency
music-assistant-models 1.1.118)Demo
Watch it live here
How it works
Setting
A single Rain Volume Ratio (%) config entry controls how loud the rain is relative to the music:
Behaviour
Architecture
Generic overlay interface on
PluginProvider:ProviderFeature.AUDIO_OVERLAY(added in models#221) lets any plugin declare overlay capability.PluginProvider.is_overlay_active(player_id) -> bool— cheap predicate the controller polls per player.PluginProvider.get_overlay_stream(player_id, pcm_format) -> AsyncGenerator[bytes, None] | None— returns a volume-adjusted PCM stream in the requested format, so the plugin handles its own conversion/scaling.The streams controller uses
mass.get_providers_supporting_feature(ProviderFeature.AUDIO_OVERLAY)to discover all candidates and mixes every active overlay together, not just the first match.Mixing via FFmpeg
amix:A new
mix_pcm_streams(inputs, pcm_format)helper inhelpers/ffmpeg.pyspawns a dedicated FFmpeg subprocess with one-i pipe:<fd>per input and-filter_complex "...amix=inputs=N:duration=first:normalize=0[mix]". Inputs are fed viaasyncio.StreamWriterwith properdrain()backpressure so the event loop is not starved when source generators yield bursty chunks.duration=firstbinds the mix length to the music input (so a looping overlay never extends playback) andnormalize=0prevents the per-input volume attenuation thatamixdoes by default.Rainy Mood specifics:
RainBufferFFmpeg subprocess runs per player for as long as rain is active, outputting raw PCM in whatever format the player's pipeline asks for (viaensure_format). Because it stays alive across track boundaries, rain content never resets between songs or on seek.scaled_streamapplies the per-instance volume ratio before yielding rain PCM into the mixer.Coverage:
Overlay injection covers all three audio paths via
wrap_overlay_if_active:serve_queue_item_stream,serve_queue_flow_stream(HTTP — used by Sonos native), andget_stream(direct PCM — used by Sendspin / AirPlay).The overlay lookup always uses
queue_id(the queue owner) rather thanplayer_id, so it works correctly when a player delegates transport to a bridge protocol (e.g. a Sonos speaker streaming via Sendspin).Test plan
🤖 Generated with Claude Code